Pesquisa — Obx vs AppGetBuilder: reatividade no NewSellStepper
Data: 2026-05-27
Contexto: Task #19323 — RF-05 (NewSellStepper reativo)
Introdução
O projeto coezzion_vendas_app define regras explícitas de reatividade no README.md:
"Todos os widgets (telas inclusas) devem ser Stateless (exceções devem ser discutidas) e toda a gerência de estado deve ser feita pelo GetX, ou seja, sem setState" (linha 55)
"Atenção: não utilizar o GetBuilder, e sim o AppGetBuilder que é o que contém as regras oficiais do projeto" (linha 108)
Esta pesquisa compara os dois mecanismos disponíveis (Obx e AppGetBuilder) para decidir qual usar na reatividade do NewSellStepper.
Mecanismos
Obx (GetX nativo)
Fonte: get-4.7.3/lib/get_state_manager/src/rx_flutter/rx_obx_widget.dart
ObxState (StatefulWidget interno)
└── _observer = RxNotifier() ← sujeito/stream próprio
└── initState: _observer.listen(_updateTree) ← assina stream
└── _updateTree: setState(() {})
└── build: RxInterface.notifyChildren(_observer, widget.build)
Como detecta mudanças:
- Durante
build(),RxInterface.proxy = _observer(global thread-local) - O builder executa; qualquer acesso a
.valuede um.obs→RxInterface.proxy?.addListener(subject)— vincula o stream da variável Rx ao observer - Quando
someObs.value = newValue→subject.add(_value)→ propaga pelo stream →_observernotificado →_updateTree→setState() - Se NENHUM
.obsfor acessado durante o build,Obxlança erro
Gatilho: someObs.value = x (automático, sem update() manual)
AppGetBuilder (custom do projeto)
Fonte: coezzion_vendas_app/lib/components/app_get_builder.dart
AppGetBuilderState (StatefulWidget interno)
└── initState: controller?.addListener(getUpdate)
└── getUpdate: if (mounted) setState(() {})
└── build: widget.builder()
Como detecta mudanças:
GetxControllerestendeListNotifier— mantém lista_updatersde callbacksaddListener(getUpdate)armazenagetUpdateem_updaters- Quando
controller.update()é chamado →ListNotifier.refresh()→_notifyUpdate()→ itera_updaters→ chama cadagetUpdate→setState() dispose: remove listener via_remove?.call(); opcionalmente chamacontroller.onScreenClosed()(se implementarDefaultApiControllerInterface)
Gatilho: controller.update() (explícito, requer chamada manual)
Comparação lado a lado
| Aspecto | Obx | AppGetBuilder |
|---|---|---|
| Fonte | GetX (pacote externo) | Custom do projeto (lib/components/) |
| Widget base | StatefulWidget (interno) | StatefulWidget (interno) |
| Reconstrução | setState(() {}) | setState(() {}) |
| Gatilho | .obs value change (automático) | controller.update() (explícito) |
| Controller necessário? | Não — funciona com qualquer .obs | Sim — GetxController é obrigatório |
| Granularidade | Sub-árvore dentro do builder | Toda a árvore do builder |
| Lifecycle hooks | Nenhum | onScreenClosed() (se DefaultApiControllerInterface) |
| Uso no projeto | 149 ocorrências, sempre aninhado dentro de AppGetBuilder | Shell externo de telas/seções |
| README manda usar? | Não mencionado explicitamente | Sim — "não utilizar o GetBuilder, e sim o AppGetBuilder" |
Cadeia de eventos: Obx
Controller: isActive.value = true
↓
RxImpl.set value(val):
if (_value == val && !firstRebuild) return; ← evita rebuilds desnecessários
_value = val;
subject.add(_value); ← emite no stream
↓
NotifyManager.addListener (registrado via proxy):
subs = rxGetx.listen((data) {
subject.add(data); ← propaga para o observer
});
↓
ObxState._observer.listen(_updateTree):
_updateTree(_) => setState(() {}); ← reconstrói sub-árvore
↓
build(): RxInterface.notifyChildren(_observer, widget.build)
→ proxy = _observer → executa builder → acessa isActive.value → registra listener novamente
→ se nenhum .obs acessado → ERRO
Cadeia de eventos: AppGetBuilder
Controller: update()
↓
GetxController.update([ids], condition):
if (!condition) return;
refresh(); ← ListNotifier.refresh()
↓
ListNotifierMixin.refresh():
_notifyUpdate();
↓
ListNotifierMixin._notifyUpdate():
for (element in _updaters) { element!(); } ← itera callbacks registrados
↓
AppGetBuilderState.getUpdate:
if (mounted) setState(() {}); ← reconstrói árvore inteira
↓
build(): widget.builder() ← re-executa o builder completo
Cenário: AppGetBuilder aninhado (tela com cartController + stepper com upsellController)
Este é o cenário real: a tela CheckSellScreen (ou NewSellScreen) usa AppGetBuilder(init: cartController, ...) como shell externo. Dentro dela, o NewSellStepper usaria AppGetBuilder(init: upsellController, ...).
AppGetBuilder(init: cartController, builder: () { ← outer
Column(children: [
NewSellStepper(currentStep: SteppSell.xxx), ← contém AppGetBuilder interno
// ... outros widgets
])
})
O que acontece quando cartController.update() dispara:
- Outer
AppGetBuilderé notificado →setState()→widget.builder()re-executa NewSellStepperé recriado (novoStatelessWidget→ novobuild())- Inner
AppGetBuilderanterior édisposed:_remove?.call()→ remove listener doupsellController._updaterscontroller = null
- Inner
AppGetBuildernovo executainitState:controller = widget.init(mesma instância lazySingleton)_subscribeToController()→controller.addListener(getUpdate)→ adiciona callback em_updaters
Custo: A cada rebuild do outer, o inner AppGetBuilder é destruído e recriado. O listener é removido e readicionado. Se o cart muda frequentemente (ex: adicionar produtos), isso acontece múltiplas vezes por venda.
O que acontece quando upsellController.update() dispara:
- Inner
AppGetBuilderé notificado →setState()→widget.builder()re-executa - Apenas o stepper é reconstruído — outer não é afetado
- Sem destruição/recriação do inner AppGetBuilder (não está sendo substituído por um novo widget)
Custo: Baixo. Rebuild apenas do stepper.
O mesmo cenário com Obx standalone:
AppGetBuilder(init: cartController, builder: () { ← outer
Column(children: [
NewSellStepper(currentStep: SteppSell.xxx), ← StatelessWidget com Obx interno
])
})
// Dentro de NewSellStepper.build():
Obx(() {
final isActive = controller.isActive.value; ← registro automático via proxy
return Row(children: [...]);
})
- Quando cart muda → outer reconstrói → stepper recriado → Obx recriado
- Obx antigo:
dispose()→_observer.close()→ streams limpos - Obx novo:
initState→ novo_observer→_observer.listen(_updateTree)→build()→notifyChildren→ registra proxy viaisActive.value
Custo: Equivalente ao AppGetBuilder aninhado. Ambos têm overhead de destroy/recreate no outer rebuild.
Decisões tomadas
| # | Questão | Decisão |
|---|---|---|
| Q1 | Obx standalone é suficiente para o stepper? | Tecnicamente sim. O stepper só precisa reagir a isActive.value. Obx faz isso sem boilerplate de update(). |
| Q2 | AppGetBuilder é obrigatório pelo README? | O README manda usar AppGetBuilder "em vez de GetBuilder" — não proíbe Obx. Mas o padrão de uso (149 Obx sempre aninhados em AppGetBuilder) sugere que AppGetBuilder é o shell esperado. |
| Q3 | O controller precisa de .obs ou só update()? | Com AppGetBuilder, o controller poderia usar apenas update() e um bool simples. Se quiser compatibilidade com Obx para outros consumidores, manter RxBool + chamar update() manualmente. |
| Q4 | Aninhamento de AppGetBuilder é problemático? | Não quebra nada, mas cada rebuild do outer causa destroy/recreate do inner. É o mesmo overhead que Obx sofreria no mesmo cenário. O projeto já faz isso em check_sell_crm_values.dart. |
| Q5 | Recomendação final | AppGetBuilder. Alinha-se ao README, ao ecossistema de controllers do projeto (toda reatividade passa por GetxController), e o custo de aninhamento é aceitável (já praticado). O controller deve expor bool isActive (não RxBool) e chamar update() quando mudar — mais simples e explícito. |